﻿using System;
using System.CodeDom.Compiler;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Xsl;
using Comdiv.QWeb.Files;
using Comdiv.QWeb.Serialization.BxlParser;
using Comdiv.QWeb.Utils;
using Microsoft.CSharp;

namespace Comdiv.QWeb.ViewEngine {
	public sealed class QViewCompiler : IQViewCompiler, IRegistryBound {
		private const string OptimizeOutRegex =
			@"(?<w1>[\r\n]\s*)outp\(""(?<s1>[^\r\n]+?)""\);\s+outp\(""(?<s2>[^\r\n]+?)""\);(?<w2>[\r\n])";

		private BxlXmlParser _bxlparser;
		private CSharpCodeProvider _codeprovider;
		private XslCompiledTransform _mainVBxlCompiler;
		private QWebServiceRegistry _registry;

		public int CompileCount { get; protected set; }

		private IFileNameResolver Files {
			get { return Registry.FileNameResolver; }
		}

		#region IQViewCompiler Members

		public Type Compile(IQViewDescriptor descriptor) {
			if (descriptor == null) throw new ArgumentNullException("descriptor");
			lock (this) {
				var sw = Stopwatch.StartNew();
				CompileCount++;


				var result = Compile(descriptor.Source);
				descriptor.LastCompilation = DateTime.Now;


				sw.Stop();
				Registry.Statistics.QViewEngineStatistics.Compilations++;
				Registry.Statistics.QViewEngineStatistics.TotalCompileTime += sw.Elapsed;
				return result;
			}
		}


		public void Reset() {
			_mainVBxlCompiler = null;
		}

		public string ConvertVBxlToCSharp(string clsname, string bxlsource) {
			var xml = ConvertVBxlToXml(bxlsource);
			PrepareMainCompiler();
			var args = new XsltArgumentList();
			args.AddParam("clsname", "", clsname);
			args.AddExtensionObject("qweb:compiler", new CompilerXsltSupport());
			var sw = new StringWriter();
			_mainVBxlCompiler.Transform(xml.CreateReader(), args, sw);
			var cs = sw.ToString();
			cs = OptimizeOutps(cs);
			return cs;
		}

		public XElement ConvertVBxlToXml(string bxlsource) {
			_bxlparser = _bxlparser ?? new BxlXmlParser().NoLexData.SafeAnonymNames;
			var xml = _bxlparser.Parse(bxlsource);
			ApplyGlobalSubst(xml);
			PreprocessBooLikeStringInterpolations(xml);
			return xml;
		}

		public IQViewDescriptor GetDescriptor(string name) {
			lock (this) {
				var result = new QViewDescriptor {Name = name, NeedRecompile = true};
				result.SetRegistry(Registry);
				if (result.Source.Sources.Length == 0) {
					throw new Exception("cannot find files for view " + name);
				}
				return result;
			}
		}

		#endregion

		#region IRegistryBound Members

		public QWebServiceRegistry Registry {
			get { return _registry; }
		}

		public void SetRegistry(QWebServiceRegistry registry) {
			_registry = registry;
			_registry.Factory.PrepareAllActionsAndRenders();
		}

		#endregion

		private Type Compile(QViewSourceDescriptor desc) {
			Assembly assembly;
			if (!desc.NeedRecompile) {
				assembly = Assembly.Load(File.ReadAllBytes(desc.LibraryFile),
				                         File.ReadAllBytes(desc.LibraryFile.Replace(".dll", ".pdb")));
			}
			else {
				assembly = BuildAssembly(desc);
			}
				var type = assembly.GetTypes().First(x => typeof (IQView).IsAssignableFrom(x));
			
			return type;
		}

		private Assembly BuildAssembly(QViewSourceDescriptor desc) {
			var sw = Stopwatch.StartNew();
			PrepareBuildList(desc);
			sw.Stop();
			Registry.Statistics.QViewEngineStatistics.PreprocessorTime += sw.Elapsed;
			sw = Stopwatch.StartNew();
			var assemblies = getReferencedAssemblies();
			var cp = new CompilerParameters(assemblies)
			         	{
			         		GenerateInMemory = false,
			         		OutputAssembly = desc.LibraryFile,
			         		IncludeDebugInformation = true,
			         		TreatWarningsAsErrors = false,
			         	};
			foreach (var res in desc.EmbededResourcesFiles) {
				cp.EmbeddedResources.Add(res);
			}
			_codeprovider = _codeprovider ?? new CSharpCodeProvider();
			var results = _codeprovider.CompileAssemblyFromFile(cp, desc.CSharpBuildFiles);
			checkErrors(results);
			sw.Stop();
			var assembly = Assembly.Load(File.ReadAllBytes(desc.LibraryFile),
			                             File.ReadAllBytes(desc.LibraryFile.Replace(".dll", ".pdb")));
			Registry.Statistics.QViewEngineStatistics.CSharpTime += sw.Elapsed;
			return assembly;
		}

		private void checkErrors(CompilerResults results) {
			if (results.Errors.HasErrors) {
				var str = results.Errors.Cast<CompilerError>().Aggregate("", (current, er) => current + (er + "\r\n"));
				throw new Exception("errors in compilation : \r\n" + str);
			}
		}

		private string[] getReferencedAssemblies() {
			var assemblies =
				AppDomain.CurrentDomain.GetAssemblies().Where(x => !x.IsDynamic && !x.FullName.Contains("__qview_lib")).Select(
					x => x.CodeBase.Replace("file:///", "")).ToArray();
			assemblies =
				assemblies.Where(
					x =>
					!assemblies.Any(
						y => UpperFilename(x) == UpperFilename(y) && y.Length < x.Length)).
					ToArray();
			return assemblies;
		}

		private static string UpperFilename(string x) {
			var fileName = Path.GetFileName(x);
			if (fileName != null) return fileName.ToUpper();
			throw new Exception("invalid path");
		}

		private void PrepareBuildList(QViewSourceDescriptor desc) {
			desc.CleanupOutput();
			desc.BuildFiles.Clear();
			var i = 1;

			foreach (var sf in desc.Sources) {
				var preprocessedcontent = Preprocess(desc.Name, sf);
				string srcfilename;
				if (preprocessedcontent.StartsWith("<!--JS")) {
					srcfilename = Path.Combine(desc.OutputDirectory, "_view_resource_script.js");
				}
				else if (preprocessedcontent.StartsWith("<!--CSS")) {
					srcfilename = Path.Combine(desc.OutputDirectory, "_view_resource_style.css");
				}
				else if (sf.EndsWith(".resb")) {
// ReSharper disable AssignNullToNotNullAttribute
					srcfilename = Path.Combine(desc.OutputDirectory, Path.GetFileName(sf));
// ReSharper restore AssignNullToNotNullAttribute
				}
				else {
					srcfilename = Path.Combine(desc.OutputDirectory, "source_" + i + ".cs");
				}

				desc.BuildFiles.Add(srcfilename);
				File.WriteAllText(srcfilename, preprocessedcontent);
				i++;
			}
		}


		private string Preprocess(string name, string filename) {
			var clsname = name.Replace("/", "_") + "_view";
			var ext = Path.GetExtension(filename);
			if (ext == ".cs") {
				return preprocessCSharp(clsname, filename);
			}
			if (ext == ".vbxl") {
				return PreprocessVBxl(clsname, filename);
			}
			if (ext == ".css") {
				return "<!--CSS--><style type='text/css'><!-- " + File.ReadAllText(filename) + " --></style>";
			}
			if (ext == ".js") {
				return "<!--JS--><script type='text/javascript'>" + File.ReadAllText(filename) + "</script>";
			}
			if (ext == ".resb") {
				return new BxlXmlParser().NoLexData.Parse(File.ReadAllText(filename)).ToString();
			}
			if (ext == ".embed") {
				return File.ReadAllText(filename);
			}
			throw new Exception("unqnown extension for view " + ext);
		}

	

		private string PreprocessVBxl(string clsname, string filename) {
			var bxlsource = File.ReadAllText(filename);
			return ConvertVBxlToCSharp(clsname, bxlsource);
		}


		private static string OptimizeOutps(string cs) {
			while (Regex.IsMatch(cs, OptimizeOutRegex, RegexOptions.Compiled)) {
				cs = Regex.Replace(cs, OptimizeOutRegex, @"${w1}outp(""${s1}${s2}"");${w2}");
			}
			return cs;
		}


		private void ApplyGlobalSubst(XElement xml) {
			foreach (var node in xml.DescendantNodes()) {
				if (node is XElement) {
					var attrs = (node as XElement).Attributes();
					foreach (var a in attrs) {
						a.Value = GlobalSubst(a.Value);
					}
				}
				else if (node is XText) {
					(node as XText).Value = GlobalSubst((node as XText).Value);
				}
			}
		}

		private static string GlobalSubst(string input) {
			return input.Replace("DICTSO", "Dictionary<string,object>")
				.Replace("DICTSS", "Dictionary<string,string>");
		}

		private static void PreprocessBooLikeStringInterpolations(XElement xml) {
			new XmlInterpolationPreprocessor().Execute(xml);
		}

		private void PrepareMainCompiler() {
			if (null != _mainVBxlCompiler) return;
			var externalfile = Files.Resolve("qviewcompiler/main.xslt");
			_mainVBxlCompiler = new XslCompiledTransform();
			if (null != externalfile) {
				_mainVBxlCompiler.Load(externalfile, XsltSettings.TrustedXslt, new FileResolverBasedXmlUrlResolver(Files));
			}
			else {
				using (
					var resourse = Assembly.GetCallingAssembly().GetManifestResourceStream("Comdiv.QWeb.ViewEngine.vbxlcompiler.xslt")) {
					if (resourse != null) {
						var xreader = XmlReader.Create(resourse);
						_mainVBxlCompiler.Load(xreader, XsltSettings.TrustedXslt, new FileResolverBasedXmlUrlResolver(Files));
					}
				}
			}
		}

		private string preprocessCSharp(string clsname, string filename) {
			var content = File.ReadAllText(filename);
			content = content.Replace("VIEWCLS", clsname);
			content = content.Replace("BASECLS", "QViewBase");
			content =
				@"
using System;
using System.Collections.Generic;
using Comdiv.QWeb;
using Comdiv.QWeb.ViewEngine;
using Comdiv.QWeb.Utils;
" +
				content;
			return content;
		}
	}
}